#include <jni.h>
#include <android/log.h>

#include <GLES2/gl2.h>
#include <GLES2/gl2ext.h>

#include <vector>
#include <string>
#include <math.h>
#include <Matrix.h>
#include <sstream>
using namespace std;

#define  LOG_TAG    "libgl2jni"
#define  LOGI(...)  __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define  LOGE(...)  __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)

//////////////////////////////////////////////////////////
///Global variables
//////////////////////////////////////////////////////////

GLuint gProgram;

GLuint gvPositionHandle = 0;
GLuint normalHandle;
GLuint mMVPMatrixHandle;
GLuint mGlobalTransformationMatrixHandle;
GLuint mViewMatrixHandle;
GLuint mInvertedViewMatrixHandle;

GLuint colorHandle;

GLuint materialHandle;

Matrix mViewMatrix;
Matrix mInvertedViewMatrix;
Matrix mModelMatrix;
Matrix mProjectionMatrix;
Matrix mGlobalTransformationMatrix;
Matrix mMVPMatrix;

float near = 0.0001;
float far = 1000;

//Will be updated on initScene
float angleOfView;
float aspectRatio;

float width;
float height;


//////////////////////////////////////////////////////////
///Loads shader
//////////////////////////////////////////////////////////

GLuint loadShader(GLenum shaderType, const char* pSource) {
    GLuint shader = glCreateShader(shaderType);
    if (shader) {
        glShaderSource(shader, 1, &pSource, NULL);
        glCompileShader(shader);
    }
    return shader;
}

//////////////////////////////////////////////////////////
///Links the two shaders to a program
//////////////////////////////////////////////////////////

GLuint createProgram(const char* pVertexSource, const char* pFragmentSource) {
    GLuint vertexShader = loadShader(GL_VERTEX_SHADER, pVertexSource);
    if (!vertexShader) {
        return 0;
    }

    GLuint pixelShader = loadShader(GL_FRAGMENT_SHADER, pFragmentSource);
    if (!pixelShader) {
        return 0;
    }

    GLuint program = glCreateProgram();
    if (program) {
        glAttachShader(program, vertexShader);
        glAttachShader(program, pixelShader);
        glLinkProgram(program);
    }
    return program;
}




//////////////////////////////////////////////////////////
///Called to initialize the scene
//////////////////////////////////////////////////////////

bool initScene(int w, int h, string gVertexShader, string gFragmentShader, float angle) {



	//AspectRatio = widht / height
	aspectRatio = (float) w /  (float) h;
	angleOfView = angle;

	width = w;
	height = h;

    gProgram = createProgram(gVertexShader.c_str(), gFragmentShader.c_str());

      mViewMatrix.loadIdentity();
      mInvertedViewMatrix.loadIdentity();
      mMVPMatrix.loadIdentity();
      mGlobalTransformationMatrix.loadIdentity();

      //Load ProjectionMatrix (orthogonal)
      mProjectionMatrix.loadIdentity();
      mProjectionMatrix.projectionMatrix(near, far, angleOfView, height, width);
      //mProjectionMatrix.orthogonalProjectionMatrix(near, far, -(width/2), width/2, -(height/2), height/2);

      mMVPMatrixHandle = glGetUniformLocation(gProgram, "u_mvp_matrix");
      mViewMatrixHandle = glGetUniformLocation(gProgram, "u_modelview_matrix");
      mInvertedViewMatrixHandle = glGetUniformLocation(gProgram, "u_inv_modelview_matrix");

      gvPositionHandle = glGetAttribLocation(gProgram, "a_position");
      normalHandle = glGetAttribLocation(gProgram, "a_normal");
      colorHandle = glGetAttribLocation(gProgram, "a_color");
      materialHandle = glGetUniformLocation(gProgram, "u_material");

    glViewport(0, 0, w, h);
    glEnable(GL_DEPTH_TEST);
    glFrontFace(GL_CCW);
    glEnable(GL_CULL_FACE);

    return true;
}



//////////////////////////////////////////////////////////
///Calculates normals for model
//////////////////////////////////////////////////////////

void calculateNormals(float *vertices, int numVertices, float *normals){

	for (int i = 0; i < numVertices / 3; i += 3){

					float p1[] = {0, 0, 0};
					p1[0] = vertices[i * 3];
					p1[1] = vertices[i * 3 + 1];
					p1[2] = vertices[i * 3 + 2];

					float p2[] = {0, 0, 0};
					p2[0] = vertices[(i+1) *3];
					p2[1] = vertices[(i+1) * 3 + 1];
					p2[2] = vertices[(i+1) * 3 + 2];

					float p3[] = {0, 0, 0};
					p3[0] = vertices[(i+2) * 3];
					p3[1] = vertices[(i+2) * 3 + 1];
					p3[2] = vertices[(i+2) * 3 + 2];



					float u[] = {p2[0] - p1[0],
								 p2[1] - p1[1],
								 p2[2] - p1[2]};

					float v[] = {p3[0] - p1[0],
								 p3[1] - p1[1],
								 p3[2] - p1[2]};

					float n[] = {u[1] * v[2] - u[2] * v[1],
								 u[2] * v[0] - u[0] * v[2],
								 u[0] * v[1] - u[1] * v[0]};

					normals[i*3] = n[0];
					normals[i*3+1] = n[1];
				    normals[i*3+2] = n[2];

				    normals[(i + 1) * 3] = n[0];
				    normals[(i + 1) * 3 + 1] = n[1];
				    normals[(i + 1) * 3 + 2] = n[2];

				    normals[(i + 2) * 3] = n[0];
				    normals[(i + 2) * 3 + 1] = n[1];
				    normals[(i + 2) * 3 + 2] = n[2];
		};
}







//////////////////////////////////////////////////////////
///Called for every model
//////////////////////////////////////////////////////////

void drawModel(float *vertices, GLuint type, GLint numVertices,
			   GLbyte *transformationTypes, float *transformationValues, GLint numTransformations,
			   float *color,
			   float *materialAmbient, float *materialDiffuse, float *materialSpecular, float materialExponent){


	//Calculate transformation-matrix for this model
	//for all transformations: transformation * modelMatrix
    mModelMatrix.loadIdentity();

	Matrix mTransformationMatrix;
	int position = 0;
	for (int i = 0; i < numTransformations; i++){
		mTransformationMatrix.loadIdentity();
		if (transformationTypes[i] == 0){
			mTransformationMatrix.rotateX(transformationValues[position]);
			position++;
			mTransformationMatrix.matrixMultiply(mTransformationMatrix, mModelMatrix, mModelMatrix);
		}
		else if (transformationTypes[i] == 1){
			mTransformationMatrix.rotateY(transformationValues[position]);
			position++;
			mTransformationMatrix.matrixMultiply(mTransformationMatrix, mModelMatrix, mModelMatrix);

		}
		else if (transformationTypes[i] == 2){
			mTransformationMatrix.rotateZ(transformationValues[position]);
			position++;
			mTransformationMatrix.matrixMultiply(mTransformationMatrix, mModelMatrix, mModelMatrix);

		}
		else if (transformationTypes[i] == 3){
			mTransformationMatrix.translate(transformationValues[position],
					    					transformationValues[position+1],
					    					transformationValues[position+2]);
		    position += 3;
		    mTransformationMatrix.matrixMultiply(mTransformationMatrix, mModelMatrix, mModelMatrix);
		}
		else if (transformationTypes[i] == 4){
			mTransformationMatrix.scale(transformationValues[position],
							    		transformationValues[position+1],
							    		transformationValues[position+2]);
			position += 3;
			mTransformationMatrix.matrixMultiply(mTransformationMatrix, mModelMatrix, mModelMatrix);
				}

	}


	//Multiply global transformation to modelMatrix
    //mModelMatrix.matrixMultiply(mModelMatrix, mGlobalTransformationMatrix, mModelMatrix);


    //MVPMatrix = (Projection * View) * ModelMatrix
    Matrix mProjectionViewMatrix;
    mProjectionViewMatrix.loadIdentity();
    mProjectionViewMatrix.matrixMultiply(mProjectionMatrix, mViewMatrix, mProjectionViewMatrix);
	mMVPMatrix.matrixMultiply(mProjectionViewMatrix, mModelMatrix, mMVPMatrix);
    //Give MVP Matrix to shader
    glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mMVPMatrix.matrix);



    //ModelViewMatrix = View * ModelMatrix
    Matrix mModelViewMatrix;
    mModelViewMatrix.loadIdentity();
    mModelViewMatrix.matrixMultiply(mViewMatrix, mModelMatrix, mModelViewMatrix);
    //Give ModelViewMatrix to shader
    glUniformMatrix4fv(mViewMatrixHandle, 1, false, mModelViewMatrix.matrix);



    //Transposed inverted ModelViewMatrix
    Matrix mInvertedTransViewMatrix;
    mInvertedTransViewMatrix.loadIdentity();
    mInvertedTransViewMatrix.transposeInvert(mModelViewMatrix);
    //Give transposed inverted ModelViewMatrix to shader
    glUniformMatrix4fv(mInvertedViewMatrixHandle, 1, false, mInvertedTransViewMatrix.matrix);





	//Give material colors to vertexshader
	GLuint locAmbient = glGetUniformLocation(gProgram, "u_material.ambient_color");
	glUniform4fv(locAmbient, 1, materialAmbient);

	GLuint locDiffuse = glGetUniformLocation(gProgram, "u_material.diffuse_color");
    glUniform4fv(locDiffuse, 1, materialDiffuse);

    GLuint locSpecular = glGetUniformLocation(gProgram, "u_material.specular_color");
    glUniform4fv(locSpecular, 1, materialSpecular);

    GLuint locExponent = glGetUniformLocation(gProgram, "u_material.specular_exponent");
    glUniform1f(locExponent, materialExponent);

    //Give colors to vertexshader
	glEnableVertexAttribArray(colorHandle);
	glVertexAttribPointer(colorHandle, 4, GL_FLOAT, GL_FALSE, 0, color);


	//Calculate normals and asign them to vertexshader
	float normals[numVertices];

	calculateNormals(vertices, numVertices, normals);

/*
	    stringstream ss;
	    for (int i = 0; i < numVertices; i++)
	    	ss << normals[i] << " dann ";


	    __android_log_write(ANDROID_LOG_ERROR,"Renderer", ss.str().c_str());
*/


	//Give normals to vertexshader
	glEnableVertexAttribArray(normalHandle);
	glVertexAttribPointer(normalHandle, 3, GL_FLOAT, GL_FALSE, 0, normals);

	//Give vertexes to vertexshader
    glEnableVertexAttribArray(gvPositionHandle);
    glVertexAttribPointer(gvPositionHandle, 3, GL_FLOAT, GL_FALSE, 0, vertices);
    glDrawArrays (GL_TRIANGLES, 0, 36);
    //glDrawElements(type, numIndices, GL_UNSIGNED_BYTE, indices);
}




//////////////////////////////////////////////////////////
///Draws the lights
//////////////////////////////////////////////////////////

void drawLight(float *lightDirection, float *lightAmbient, float *lightDiffuse, float *lightSpecular){

	GLuint locDirection = glGetUniformLocation(gProgram, "u_light.direction");
    glUniform3fv(locDirection, 1, lightDirection);

    GLuint locHalf = glGetUniformLocation(gProgram, "u_light.halfplane");
    glUniform3fv(locHalf, 1, lightDirection);

    GLuint locAmbient = glGetUniformLocation(gProgram, "u_light.ambient_color");
    glUniform4fv(locAmbient, 1, lightAmbient);

    GLuint locDiffuse = glGetUniformLocation(gProgram, "u_light.diffuse_color");
    glUniform4fv(locDiffuse, 1, lightDiffuse);

    GLuint locSpecular = glGetUniformLocation(gProgram, "u_light.specular_color");
    glUniform4fv(locSpecular, 1, lightSpecular);

}




int degree = 0;
//////////////////////////////////////////////////////////
///Initializes the frame
//////////////////////////////////////////////////////////

void initFrame() {

	if (degree >= 359)
		degree = 0;

	degree++;

    glClearColor(0, 0, 0, 0.0f);
    glClear( GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);

    glUseProgram(gProgram);



    mGlobalTransformationMatrix.loadIdentity();
    Matrix rotY;
    rotY.rotateY(degree);
    Matrix rotX;
    rotX.rotateX(degree);
    Matrix rotZ;
    rotZ.rotateZ(degree);
    //mGlobalTransformationMatrix.matrixMultiply(rotX, rotY, mGlobalTransformationMatrix);
    mGlobalTransformationMatrix.matrixMultiply(rotX, mGlobalTransformationMatrix, mGlobalTransformationMatrix);
}







//////////////////////////////////////////////////////////
///Finishes the frame
//////////////////////////////////////////////////////////

void finishFrame(){



}









//////////////////////////////////////////////////////////
///C-Code linking to java
//////////////////////////////////////////////////////////

extern "C" {
    JNIEXPORT void JNICALL Java_rendercomponents_NativeRenderer_initScene(JNIEnv * env, jobject obj,  jint width, jint height,
    																	  jstring vertexShader, jstring fragmentShader,
    																	  jfloat angle);


    JNIEXPORT void JNICALL Java_rendercomponents_NativeRenderer_initFrame(JNIEnv * env, jobject obj);


    JNIEXPORT void JNICALL Java_rendercomponents_NativeRenderer_drawModel(JNIEnv * env, jobject obj,
        		jfloatArray vertices, jint numVertices,
        		jbyteArray transformationTypes, jfloatArray transformationValues, jint numTransformations,
        		jfloatArray color,
        	    jfloatArray materialAmbient, jfloatArray materialDiffuse, jfloatArray materialSpecular, jfloat materialExponentIn);


    JNIEXPORT void JNICALL Java_rendercomponents_NativeRenderer_drawLight(JNIEnv * env, jobject obj,
    		 jfloatArray lightDirection, jfloatArray lightAmbient, jfloatArray lightDiffuse, jfloatArray lightSpecular);


    JNIEXPORT void JNICALL Java_rendercomponents_NativeRenderer_finishFrame(JNIEnv * env, jobject obj);



    JNIEXPORT void JNICALL Java_rendercomponents_NativeRenderer_setCamera(JNIEnv * env, jobject obj,
    		jfloat viewMatrix0, jfloat viewMatrix1, jfloat viewMatrix2, jfloat viewMatrix3,
    		jfloat viewMatrix4, jfloat viewMatrix5, jfloat viewMatrix6, jfloat viewMatrix7,
    		jfloat viewMatrix8, jfloat viewMatrix9, jfloat viewMatrix10, jfloat viewMatrix11,
    		jfloat viewMatrix12, jfloat viewMatrix13, jfloat viewMatrix14, jfloat viewMatrix15);
};

JNIEXPORT void JNICALL Java_rendercomponents_NativeRenderer_initScene(JNIEnv * env, jobject obj,  jint width, jint height,
																	  jstring vertexShader, jstring fragmentShader,
																	  jfloat angle)
{
	jboolean blnIsCopy;
	jboolean bInFragIsCopy;
	const char* vertexIn = (env)->GetStringUTFChars(vertexShader , &blnIsCopy);
	const char* fragmentIn = (env)->GetStringUTFChars(fragmentShader , &bInFragIsCopy);
    initScene(width, height, vertexIn, fragmentIn, angle);

    (env)->ReleaseStringUTFChars(vertexShader, vertexIn);
    (env)->ReleaseStringUTFChars(fragmentShader, fragmentIn);
}


JNIEXPORT void JNICALL Java_rendercomponents_NativeRenderer_initFrame(JNIEnv * env, jobject obj)
{

	initFrame();

}

JNIEXPORT void JNICALL Java_rendercomponents_NativeRenderer_finishFrame(JNIEnv * env, jobject obj)
{

	finishFrame();

}



JNIEXPORT void JNICALL Java_rendercomponents_NativeRenderer_drawModel(JNIEnv * env, jobject obj,
		jfloatArray vertices, jint numVertices,
		jbyteArray transformationTypes, jfloatArray transformationValues, jint numTransformations,
		jfloatArray color,
	    jfloatArray materialAmbient, jfloatArray materialDiffuse, jfloatArray materialSpecular, jfloat materialExponentIn)
{

	jfloat *verticesIn = (env)->GetFloatArrayElements(vertices, false);
	//jbyte *indicesIn = (env)->GetByteArrayElements(indices, false);
	//jfloat *normalsIn = (env)->GetFloatArrayElements(normals, false);
	jbyte *transformationTypesIn = (env)->GetByteArrayElements(transformationTypes, false);
	jfloat *transformationValuesIn = (env)->GetFloatArrayElements(transformationValues, false);
	jfloat *colorIn = (env)->GetFloatArrayElements(color, false);
	jfloat *materialAmbientIn = (env)->GetFloatArrayElements(materialAmbient, false);
	jfloat *materialDiffuseIn = (env)->GetFloatArrayElements(materialDiffuse, false);
	jfloat *materialSpecularIn = (env)->GetFloatArrayElements(materialSpecular, false);

	drawModel(verticesIn,
			GL_TRIANGLES,
			numVertices,
			transformationTypesIn,
			transformationValuesIn,
			numTransformations,
			colorIn,
			materialAmbientIn,
			materialDiffuseIn,
			materialSpecularIn,
			materialExponentIn);


    (env)->ReleaseFloatArrayElements(vertices, verticesIn, 0);
   // (env)->ReleaseByteArrayElements(indices, indicesIn, 0);
   // (env)->ReleaseFloatArrayElements(normals, normalsIn, 0);
    (env)->ReleaseByteArrayElements(transformationTypes, transformationTypesIn, 0);
    (env)->ReleaseFloatArrayElements(transformationValues, transformationValuesIn, 0);
    (env)->ReleaseFloatArrayElements(color, colorIn, 0);
    (env)->ReleaseFloatArrayElements(materialAmbient, materialAmbientIn, 0);
    (env)->ReleaseFloatArrayElements(materialDiffuse, materialDiffuseIn, 0);
    (env)->ReleaseFloatArrayElements(materialSpecular, materialSpecularIn, 0);
}


JNIEXPORT void JNICALL Java_rendercomponents_NativeRenderer_drawLight(JNIEnv * env, jobject obj,
		jfloatArray lightDirection, jfloatArray lightAmbient, jfloatArray lightDiffuse, jfloatArray lightSpecular)
{

	jfloat *lightDirectionIn = (env)->GetFloatArrayElements(lightDirection, false);
	jfloat *lightAmbientIn = (env)->GetFloatArrayElements(lightAmbient, false);
	jfloat *lightDiffuseIn = (env)->GetFloatArrayElements(lightDiffuse, false);
	jfloat *lightSpecularIn = (env)->GetFloatArrayElements(lightSpecular, false);

	drawLight(lightDirectionIn,
			  lightAmbientIn,
			  lightDiffuseIn,
			  lightSpecularIn);


    (env)->ReleaseFloatArrayElements(lightDirection, lightDirectionIn, 0);
    (env)->ReleaseFloatArrayElements(lightAmbient, lightAmbientIn, 0);
    (env)->ReleaseFloatArrayElements(lightDiffuse, lightDiffuseIn, 0);
    (env)->ReleaseFloatArrayElements(lightSpecular, lightSpecularIn, 0);
}

JNIEXPORT void JNICALL Java_rendercomponents_NativeRenderer_setCamera(JNIEnv * env, jobject obj,
		jfloat viewMatrix0, jfloat viewMatrix1, jfloat viewMatrix2, jfloat viewMatrix3,
		jfloat viewMatrix4, jfloat viewMatrix5, jfloat viewMatrix6, jfloat viewMatrix7,
		jfloat viewMatrix8, jfloat viewMatrix9, jfloat viewMatrix10, jfloat viewMatrix11,
		jfloat viewMatrix12, jfloat viewMatrix13, jfloat viewMatrix14, jfloat viewMatrix15)
{
	float matrixIn[16];

	matrixIn[0] = viewMatrix0;
	matrixIn[1] = viewMatrix1;
	matrixIn[2] = viewMatrix2;
	matrixIn[3] = viewMatrix3;
	matrixIn[4] = viewMatrix4;
	matrixIn[5] = viewMatrix5;
	matrixIn[6] = viewMatrix6;
	matrixIn[7] = viewMatrix7;
	matrixIn[8] = viewMatrix8;
	matrixIn[9] = viewMatrix9;
	matrixIn[10] = viewMatrix10;
	matrixIn[11] = viewMatrix11;
	matrixIn[12] = viewMatrix12;
	matrixIn[13] = viewMatrix13;
	matrixIn[14] = viewMatrix14;
	matrixIn[15] = viewMatrix15;

	//far = matrixIn[10] * 2.0f;


    mProjectionMatrix.projectionMatrix(near, far, angleOfView, height, width);

	mViewMatrix.setMatrix(matrixIn);
}
